# 路由插件注册

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

源码如下:

function install (Vue) {
  if (install.installed && _Vue === Vue) { return }
  install.installed = true;

  _Vue = Vue;

  var isDef = function (v) { return v !== undefined; };

  var registerInstance = function (vm, callVal) {
    var i = vm.$options._parentVnode;
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal);
    }
  };

  Vue.mixin({
    beforeCreate: function beforeCreate() {
      // 注册路由实例
      if (isDef(this.$options.router)) {
        this._routerRoot = this;
        this._router = this.$options.router;
        this._router.init(this);
        // 对 _route 进行相应式处理
        Vue.util.defineReactive(this, '_route', this._router.history.current);
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
      }
      registerInstance(this, this);
    },
    destroyed: function destroyed () {
      // 销毁路由实例
      registerInstance(this);
    }
  });

  // 向 vue 原型上挂载 $router 和 $route
  Object.defineProperty(Vue.prototype, '$router', {
    get: function get () { return this._routerRoot._router }
  });
  Object.defineProperty(Vue.prototype, '$route', {
    get: function get () { return this._routerRoot._route }
  });
	
  // 注册全局组件 RouterView 和 RouterLink
  Vue.component('RouterView', View);
  Vue.component('RouterLink', Link);

  var strats = Vue.config.optionMergeStrategies;
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;
}

首先,是判断逻辑,确保插件只注册一次。

然后,通过 Vue.mixin注入 vue 生命周期钩子beforeCreatedestroyed

  • beforeCreate:设置根路由;注册路由实例。
  • destroyed:销毁路由实例。

接着,向 vue 原型上挂载 $router$route

接着,注册全局路由组件RouterLinkRouterView

最后,对相关钩子进行合并 (opens new window)

# 实例化路由对象

export default new VueRouter({
  routes: [
    name: 'Home',
    path: '/',
    component: () => import('../view/home.vue')
  ]
})

主要完成以下逻辑:

  • 处理参数;
  • 创建路由匹配器 matcher;
  • 根据不同的路由模式 mode 实例化对应的路由模式对象。
var VueRouter = function VueRouter(options) {
  if ( options === void 0 ) options = {};

  if (process.env.NODE_ENV !== 'production') {
    warn(this instanceof VueRouter, "Router must be called with the new operator.");
  }
  
  // 参数处理
  this.app = null;
  this.apps = [];
  this.options = options;
  this.beforeHooks = [];
  this.resolveHooks = [];
  this.afterHooks = [];
  this.matcher = createMatcher(options.routes || [], this); // 创建路由匹配器 matcher

  var mode = options.mode || 'hash';
  this.fallback =
    mode === 'history' && !supportsPushState && options.fallback !== false;
  if (this.fallback) {
    mode = 'hash';
  }
  if (!inBrowser) {
    mode = 'abstract';
  }
  this.mode = mode;

  // 根据不同的路由模式 mode 实例化对应的路由模式对象。
  switch (mode) {
    case 'history':
      this.history = new HTML5History(this, options.base);
      break
    case 'hash':
      this.history = new HashHistory(this, options.base, this.fallback);
      break
    case 'abstract':
      this.history = new AbstractHistory(this, options.base);
      break
    default:
      if (process.env.NODE_ENV !== 'production') {
        assert(false, ("invalid mode: " + mode));
      }
  }
};

# 创建路由匹配器 matcher

该函数传递参数路由表当前路由对象,主要完成:

  • 创建路由映射表;
  • 返回包含 match、addRoute、getRoutes、addRoutes 四个函数的路由匹配器对象。
function createMatcher (
  routes,
  router
) {
  var ref = createRouteMap(routes); // 创建路由映射表
  var pathList = ref.pathList;
  var pathMap = ref.pathMap;
  var nameMap = ref.nameMap;
    
  // ... 一些函数定义
  function addRoutes(routes) {
    // ...
  }
  function addRoute (parentOrRoute, route) {
    // ...
  }
  function getRoutes () {
    return pathList.map(function (path) { return pathMap[path]; })
  }
  function match (
    raw,
    currentRoute,
    redirectedFrom
  ) {
    // ...
  }
  function redirect (
    record,
    location
  ) {
    // ...
  }
  function alias (
    record,
    location,
    matchAs
  ) {
    // ...
  }
  function _createRoute (
    record,
    location,
    redirectedFrom
  ) {
    // ...
  }
  
  // 返回包含 match、addRoute、getRoutes、addRoutes 四个函数的路由匹配器对象
  return {
    match: match,
    addRoute: addRoute,
    getRoutes: getRoutes,
    addRoutes: addRoutes
  }
}

# 创建路由映射表

该函数 createRouteMap 主要建立命名路由和路由路径的映射表。

如路由表为:

new VueRouter({
  routes: [
    {
      name: 'Home',
      path: getRoute(process.env.LANG).home,
      component: () => import('../views/home.vue'),
      children: [
        {
          path: 'subHome',
          name: 'SubHome',
          component: () => import('../views/subHome.vue')
        }
      ]
    }
  ]
})

生成的路由映射关系为:

ref = {
    "pathList": [
        "/home/subHome",
        "/home"
    ],
    "pathMap": {
        "/home/subHome": {
            "path": "/home/subHome",
            "regex": {
                "keys": []
            },
            "components": {}, // 对应路由组件
            "alias": [],
            "instances": {},
            "enteredCbs": {},
            "name": "SubHome",
            "parent": {
                "path": "/home",
                "regex": {
                    "keys": []
                },
                "components": {},
                "alias": [],
                "instances": {},
                "enteredCbs": {},
                "name": "Home",
                "meta": {},
                "props": {}
            },
            "meta": {},
            "props": {}
        },
        "/home": {
            "path": "/home",
            "regex": {
                "keys": []
            },
            "components": {},
            "alias": [],
            "instances": {},
            "enteredCbs": {},
            "name": "Home",
            "meta": {},
            "props": {}
        }
    },
    "nameMap": {
        "SubHome": {
            "path": "/home/subHome",
            "regex": {
                "keys": []
            },
            "components": {},
            "alias": [],
            "instances": {},
            "enteredCbs": {},
            "name": "SubHome",
            "parent": {
                "path": "/home",
                "regex": {
                    "keys": []
                },
                "components": {},
                "alias": [],
                "instances": {},
                "enteredCbs": {},
                "name": "Home",
                "meta": {},
                "props": {}
            },
            "meta": {},
            "props": {}
        },
        "Home": {
            "path": "/home",
            "regex": {
                "keys": []
            },
            "components": {},
            "alias": [],
            "instances": {},
            "enteredCbs": {},
            "name": "Home",
            "meta": {},
            "props": {}
        }
    }
}

可见,即使是嵌套路由,都会扁平化为一级。每个 key(路由路径和路由名)都包含以下基本信息:

{
  "path": "",
  "regex": {
    "keys": []
  },
  "components": {},
  "alias": [],
  "instances": {},
  "enteredCbs": {},
  "name": "Home",
  "meta": {},
  "props": {},
  "parent": {} 
}

createRouteMap 函数内部逻辑为:

  • 遍历路由表 routes,对每个路由项调用 addRouteRecord 函数进行处理,返回上面的基本信息;
  • 如果当前路由项包含 children(子路由),则遍历子路由进行递归调用 addRouteRecord 处理;
  • 如果当前路由包含 alias 别名,则把该别名当作新路由名,递归调用 addRouteRecord 处理。

createRouteMap 主要源码逻辑:

function createRouteMap (
  routes,
  oldPathList,
  oldPathMap,
  oldNameMap,
  parentRoute
) {
	// ...
  
  var pathList = oldPathList || [];
  var pathMap = oldPathMap || Object.create(null);
  var nameMap = oldNameMap || Object.create(null);
  routes.forEach(function (route) {
    addRouteRecord(pathList, pathMap, nameMap, route, parentRoute);
  }); 

  // 通配符 * 处理,确保在路由表的最后
  for (var i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === '*') {
      pathList.push(pathList.splice(i, 1)[0]);
      l--;
      i--;
    }
  }
    
  // ...  
  return {
    pathList: pathList,
    pathMap: pathMap,
    nameMap: nameMap
  }   
}

function addRouteRecord (
  pathList,
  pathMap,
  nameMap,
  route,
  parent,
  matchAs
) {
	var path = route.path;
  var name = route.name;

  var pathToRegexpOptions =
    route.pathToRegexpOptions || {};
  var normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict); // 规范化路径

  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive;
  }

  // 映射表基本信息
  var record = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
    components: route.components || { default: route.component },
    alias: route.alias
      ? typeof route.alias === 'string'
        ? [route.alias]
        : route.alias
      : [],
    instances: {},
    enteredCbs: {},
    name: name,
    parent: parent,
    matchAs: matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {},
    props:
      route.props == null
        ? {}
        : route.components
          ? route.props
          : { default: route.props }
  };

  if (route.children) {
    // ...
    
    // 遍历子路由列表,递归处理子路由
    route.children.forEach(function (child) {
      var childMatchAs = matchAs
        ? cleanPath((matchAs + "/" + (child.path)))
        : undefined;
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs);
    });
  }
	
  // 添加到路由映射表
  if (!pathMap[record.path]) {
    pathList.push(record.path);
    pathMap[record.path] = record;
  }
  
    
  // 路由别名处理,相当于一个新路由名,也是递归调用 addRouteRecord
  if (route.alias !== undefined) {
    var aliases = Array.isArray(route.alias) ? route.alias : [route.alias];
    for (var i = 0; i < aliases.length; ++i) {
      var alias = aliases[i];
      // ... 

      var aliasRoute = {
        path: alias,
        children: route.children
      };
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute,
        parent,
        record.path || '/' // matchAs
      );
    }
  }

  if (name) {
    if (!nameMap[name]) {
      nameMap[name] = record;
    } else if (process.env.NODE_ENV !== 'production' && !matchAs) {
      warn(
        false,
        "Duplicate named routes definition: " +
          "{ name: \"" + name + "\", path: \"" + (record.path) + "\" }"
      );
    }
  }    
}

# 添加到 vue 实例

在 main.js 中添加 router 实例,并创建 vue 对象。

new Vue({
	router,
	// ...
})

当实例化 Vue 对象的时候,之前通过插件机制注入的 mixin 逻辑就会执行。

function install (Vue) {
  // ...

  Vue.mixin({
    beforeCreate: function beforeCreate() {
      if (isDef(this.$options.router)) {
        this._routerRoot = this; // 
        this._router = this.$options.router;
        this._router.init(this);
        Vue.util.defineReactive(this, '_route', this._router.history.current);
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
      }
      registerInstance(this, this);
    },
    destroyed: function destroyed () {
      registerInstance(this);
    }
  });

  // ...
}

这里主要执行:

this._routerRoot = this; // _routerRoot 指向当前 Vue 实例对象
this._router = this.$options.router; // _router 指向 VueRouter 实例对象
this._router.init(this); // 执行初始化逻辑
Vue.util.defineReactive(this, '_route', this._router.history.current); // 将 _route 设置为响应式
  • 将 _routerRoot 指向当前 Vue 实例对象;
  • 将 _router 指向 VueRouter 实例对象;
  • 调用路由初始化函数;
  • 将 _route 设置为响应式。

# 路由初始化 init 函数

该函数定义在 VueRouter 原型上,主要源码如下:

VueRouter.prototype.init = function init(app /* Vue component instance */) {
  var this$1 = this;
  
  // ...

  this.apps.push(app);

  // ...

  // 如果 vue 实例已经被初始化过了,直接返回。因为路由监听器只需要一个
  if (this.app) {
    return
  }

  this.app = app;

  var history = this.history; // 获取当前路由模式对象

  if (history instanceof HTML5History || history instanceof HashHistory) {
    var handleInitialScroll = function (routeOrError) {
      var from = history.current;
      var expectScroll = this$1.options.scrollBehavior;
      var supportsScroll = supportsPushState && expectScroll;

      if (supportsScroll && 'fullPath' in routeOrError) {
        handleScroll(this$1, routeOrError, from, false);
      }
    };
    var setupListeners = function (routeOrError) {
      history.setupListeners();
      handleInitialScroll(routeOrError);
    };
    // 调用 transitionTo,获取当前浏览器 location,更新对应路由信息
    history.transitionTo(
      history.getCurrentLocation(),
      setupListeners,
      setupListeners
    );
  }

  history.listen(function (route) {
    this$1.apps.forEach(function (app) {
      app._route = route;
    });
  });
};

init 函数主要逻辑:获取当前 vue 实例,并确保初始化逻辑只执行一次(因为路由监听器只需要一个);然后获取对应的路由模式对象,调用 transitionTo 函数,获取当前浏览器 location,更新对应路由信息。

transitionTo 函数比较复杂,后面单独拎出来看源码。

# 渲染路由组件

执行代码逻辑为:

<template>
	<div id="app">
  	<router-view />
  </div>
</template>

<script>
  export default {
    name: 'App'
  }
</script>

路由组件,主要是调用组件<router-view />进行渲染,该组件在 VueRouter 插件机制已完成全局组件注册。

function install (Vue) {
  // ...
	
  // 注册全局组件 RouterView
  Vue.component('RouterView', View);
  
  // ...
}

RouterView 组件主要逻辑为:

  • 查到路由配置表,匹配到对应的路由信息;
  • 通过 $createElement 渲染组件。

RouterView 组件源码:

var View = {
  name: 'RouterView',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render: function render(_, ref) {
    var props = ref.props;
    var children = ref.children;
    var parent = ref.parent;
    var data = ref.data;

    // 设置是 routerView 组件标志 
    data.routerView = true;

    // 直接使用父组件的 createElement() 方法,这样 router-view 渲染的组件就可以解析命名槽
    var h = parent.$createElement;
    var name = props.name;
    var route = parent.$route;
    var cache = parent._routerViewCache || (parent._routerViewCache = {});

    var depth = 0;
    var inactive = false;
    
    // 循环查找父实例,获取注册 router 实例的根 vue 实例,以及当前 router-view 组件的层级 depth,通过该层级 depth 可以在 route.matched 匹配到对应的路由组件
    while (parent && parent._routerRoot !== parent) {
      var vnodeData = parent.$vnode ? parent.$vnode.data : {};
      if (vnodeData.routerView) {
        depth++;
      }
      if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
        inactive = true;
      }
      parent = parent.$parent;
    }
    data.routerViewDepth = depth;

    // keep-alive 处理
    if (inactive) {
      var cachedData = cache[name];
      var cachedComponent = cachedData && cachedData.component;
      if (cachedComponent) {
        // #2301
        // pass props
        if (cachedData.configProps) {
          fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps);
        }
        return h(cachedComponent, data, children)
      } else {
        // render previous empty view
        return h()
      }
    }

		// 获取对应的路由组件
    var matched = route.matched[depth];
    var component = matched && matched.components[name];

    // 空路由组件判断处理
    if (!matched || !component) {
      cache[name] = null;
      return h()
    }

    // 缓存匹配到的路由组件
    cache[name] = { component: component };

    // 在 data 对象上定义 registerRouteInstance 注册路由实例方法,该方法通过插件机制注入 mixin 中的 beforeCreate 钩子执行
    data.registerRouteInstance = function (vm, val) {
      var current = matched.instances[name];
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        matched.instances[name] = val;
      }
    }

    // ...
	
    // 渲染组件
    return h(component, data, children)
  }
};

以上代码主要执行:

  • 设置是 router-view 组件标志(用于后面获取当前 router-view 组件层级 depth);
  • 获取 router-view 组件的父组件及对应的 $createElement 方法。通过父组件的 $createElement 方法渲染路由组件, 是为了 router-view 渲染的组件就可以解析命名槽;
  • 循环查找父实例,获取注册 router 实例的根 vue 实例,以及当前 router-view 组件的层级 depth,通过该层级 depth 可以在 route.matched 匹配到当前 location 对应的路由组件;
  • 获取对应的路由组件;
  • 空路由组件判断处理;
  • 缓存匹配到的路由组件;
  • 在 data 对象上定义 registerRouteInstance 注册路由实例方法,该方法通过插件机制注入 mixin 中的 beforeCreate 钩子执行;
  • 通过父组件的 $createElement 方法渲染路由组件。

上面的倒数第二点,函数调用逻辑如下:

var registerInstance = function (vm, callVal) {
  var i = vm.$options._parentVnode;
  if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
    i(vm, callVal);
  }
};

Vue.mixin({
  beforeCreate: function beforeCreate() {
    if (isDef(this.$options.router)) {
      this._routerRoot = this;
      this._router = this.$options.router;
      this._router.init(this);
      Vue.util.defineReactive(this, '_route', this._router.history.current);
    } else {
      this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
    }
    registerInstance(this, this);
  },
  destroyed: function destroyed () {
    registerInstance(this);
  }
});

在 router-view 组件的 render 函数添加打印,输出 parent :console.log('>> router-view render parent:', parent);

在 mixin 的 registerInstance 中打印信息:console.log('>> vue mixin beforeCreate registerInstance:', vm, callVal);

得到打印输出如下:

>> router-view render parent: VueComponent {_uid: 1, _isVue: true, $options: {}, _renderProxy: Proxy, _self: VueComponent,}
>> vue mixin beforeCreate registerInstance: VueComponent {_uid: 2, _isVue: true, $options: {}, _renderProxy: Proxy, _self: VueComponent,}
VueComponent {_uid: 2, _isVue: true, $options: {}, _renderProxy: Proxy, _self: VueComponent,}

在实际的调试中,发现每次打印的最开始都会多一次输出:

>> router-view render parent: VueComponent {_uid: 1, _isVue: true, $options: {}, _renderProxy: Proxy, _self: VueComponent,}

在 router-view 组件的 render 函数添加打印,输出 routes:console.log('>> router-view render route:', route);,得到输出:

ViewRouter render route: {name: null, meta: {}, path: '/', hash: '', query: {},}

/ 根路径在路由表中并没有配置,但是每次都输出了。通过 debugger 发现,在源码中,会有一个 router-view 的调用逻辑。源码如下:

function createRoute (
 record,
 location,
 redirectedFrom,
 router
) {
  var stringifyQuery = router && router.options.stringifyQuery;

  var query = location.query || {};
  try {
    query = clone(query);
  } catch (e) {}

  var route = {
    name: location.name || (record && record.name),
    meta: (record && record.meta) || {},
    path: location.path || '/',
    hash: location.hash || '',
    query: query,
    params: location.params || {},
    fullPath: getFullPath(location, stringifyQuery),
    matched: record ? formatMatch(record) : []
  };
  if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery);
  }

  return Object.freeze(route)
}


// the starting route that represents the initial state
var START = createRoute(null, {
  path: '/'
});

流程为:

  • 当 init 路由初始化函数(将 VueRouter 添加到 vue 实例阶段)还没获取当前 location 之前,router-view 执行一次,使用初始路径/进行路由组件渲染,所以没有匹配到对应组件;
  • 当 init 路由初始化函数获取当前 location 并更新当前路由信息时,router-view 会再执行一次。